import hashlib
import time
import json
import requests
import signal
import threading
import socket
import random
import multiprocessing
from urllib.parse import urlparse

listenflag = True

# a block contains the index, timestamp, tx, prev hash, hash and nonce
# now the block doesn't support the minning function, it will just continuously output the block
class Block:
    def __init__(self, index, timestamp, tx, PrevHash, nonce, hash):
        self.index = index
        self.timestamp = timestamp
        self.tx = tx
        self.PrevHash = PrevHash
        if(nonce!=-1):
            self.nonce = nonce
        else:
            self.nonce = random.randint(0,65535)
        if(hash!=0):
            self.hash = hash
        else:
            self.hash = self.CalculateHash()

    # this func helps to generate the previous block's hash
    def CalculateHash(self):
        data = str(self.index) + str(self.timestamp) + str(self.PrevHash) + str(self.tx) + str(self.nonce)
        return hashlib.sha256(data.encode()).hexdigest()

    # this func will help convert the block to the json type
    def to_dict(self):
        return {
            'index': self.index,
            'timestamp' : self.timestamp,
            'tx' : self.tx,
            'previous_hash' : self.PrevHash,
            'nonce' : self.nonce,
            'hash' : self.hash
        }

# the blockchain helps remain the whole chain where all the block exists
# every next block will have the previous block's hash which called the PrevHash
# now it will contains the whole blockchain and a set of nodes
class Blockchain:
    def __init__(self):
        self.chain = []

    # func CreateGenesisBlock will create the genesis block whose index is 0 and the tx is "The Genesis Block"
    def CreateGenesisBlock(self):
        block = Block(0, time.time(), "The Genesis Block", 0, -1, 0)
        return block

    # func Check will check whether this block is valid
    def CheckTheNewestBlock(self, Block):
        LastIndex = self.GetLatestBlock().index
        if Block.index > LastIndex:
            if(Block.index - LastIndex == 1):
                if self.chain[LastIndex].hash == Block.PrevHash:
                    return 1
                else:
                    return 0
            elif Block.index - LastIndex == 0:
                self.chain[LastIndex]  = Block
                return 2
            else:
                return 4
        else:
            return 3

    def CheckTheBlock(self, Block):
        if Block.index == 0:
            self.chain.append(Block)
            return 1
        LastIndex = self.GetLatestBlock().index
        print("lastIndex is {}\n\n\n\n".format(LastIndex))
        if Block.index <= LastIndex:
            if(Block.PrevHash == self.chain[Block.index-1].hash):
                self.chain[Block.index] = Block
                return 1
            else:
                return 0
        elif Block.index == LastIndex + 1:
            if Block.PrevHash == self.chain[Block.index-1].hash:
                self.chain.append(Block)
                return 1
            else:
                return 0
        else:
            return 0

    # func AddBlock will first check if this new block is valid
    # and then insert this new block to the end of the chain
    def AddBlock(self, Block):
        self.chain.append(Block)

    # func GetLatestBlock will return the latest block
    def GetLatestBlock(self):
        return self.chain[-1]

    # this func is just for test, it will print the latest block
    def PrintLatestBlock(self):
        block = self.GetLatestBlock()
        print("Block:{}\n\ttimestamp:{}\n\ttx:{}\n\tPrevHash:{}\n\tHash:{}\n\tNonce:{}\n\t".format(block.index, block.timestamp, block.tx, block.PrevHash, block.hash, block.nonce))

# here is the struct Peer
# Peer acts as a node which maintains the network and send the messages
# As the anchor peer, it should 
class Peer:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.status = 0
        self.MaxIndex = -1
        self.blockchain = Blockchain()
        self.peers = []
        self.transaction = []
    
    def start_listening(self):
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_socket.bind((self.host, self.port))
        server_socket.listen(5)

        while listenflag:
            client_socket, address = server_socket.accept()
            print(f"Accepted connection from {address[0]}:{address[1]}")
            client_thread = threading.Thread(target=self.handle_connection, args=(client_socket,))
            client_thread.start()

    def handle_connection(self, client_socket):
        while listenflag:
            data = client_socket.recv(1024).decode()
            if data:
                print(f"Received message: {data}")
                message = json.loads(data)
                self.process_message(message)
            else:
                break
        client_socket.close()

    def send_message(self, host, port, message):
        client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        again = True
        while again:
            again = False
            try:
                client_socket.connect((host, port))
            except OSError as e:
                again = True
            finally:
                continue
        MessageToSend = json.dumps(message)
        client_socket.send(MessageToSend.encode())
        client_socket.close()

    def process_message(self, message):
        command = message.get('command')
        # handle the command: add block
        if command == 'add_block' and self.status==1:
            block_data = message.get('block')
            block = Block(block_data['index'], block_data['timestamp'],
                          block_data['tx'], block_data['previous_hash'], block_data['nonce'], block_data['hash'])
            ip = message.get('ip')
            host = ip['host']
            port = ip['port']
            x = self.blockchain.CheckTheNewestBlock(block)
            if x==1:
                self.MaxIndex = block.index
                self.blockchain.AddBlock(block)
                self.broadcast_block(block)
                self.blockchain.PrintLatestBlock()
            elif x==2:
                self.broadcast_block(block)
            elif x==3:
                print("the chain is outdated, please check it!")
            elif x==4:
                self.status = 2
                self.MaxIndex = block.index
                
                message = {
                    'command' : 'AskForSynchronization',
                    'index' : 0,
                    'ip' : {
                        'host' : self.host,
                        'port' : self.port
                    }
                }
                self.send_message(host, port, message)
        # handle the command: add transaction
        elif command == 'add_transaction':
            transaction = message.get('transaction')
            self.transaction.append(transaction)
            print("Added transaction to pending list:", transaction)
        # handle the command: initialize
        elif command == 'initialize':
            ip = message.get('ip')
            self.initalize(ip)
        # handle the command: Get the max index
        elif command == "AskForMaxIndex":
            ip = message.get('ip')
            host = ip['host']
            port = ip['port']
            MaxIndex = self.blockchain.GetLatestBlock().index
            message = {
                'command' : 'TheMaxIndex',
                'index' : MaxIndex
            }
            self.send_message(host, port, message)

        elif command == "TheMaxIndex":
            maxindex = message.get('index')
            self.MaxIndex = max(self.MaxIndex, maxindex)

        # synchronization command gives the index which wants to get the corresponding block
        elif command == "AskForSynchronization":
            index = message.get('index')
            ip = message.get('ip')
            host = ip['host']
            port = ip['port']
            block = self.blockchain.chain[index]
            message = {
                'command' : 'SynchronizationBlock',
                'block' : block.to_dict(),
                'ip' : {
                    'host' : self.host,
                    'port' : self.port
                }
            }
            self.send_message(host, port, message)

        elif command == "SynchronizationIP":
            IPs = message.get('peers')
            self.peers = IPs

        elif command == "addIP":
            ip = message.get('ip')
            self.peers.append(ip)
        # get the block
        elif command == "SynchronizationBlock":
            block_data = message.get('block')
            block = Block(block_data['index'], block_data['timestamp'],block_data['tx'], block_data['previous_hash'], block_data['nonce'], block_data['hash'])
            ip = message.get('ip')
            host = ip['host']
            port = ip['port']
            x = self.blockchain.CheckTheBlock(block)
            if x==0:
                # restart to ask for synchronization
                self.MaxIndex = 0
                self.status = 0
                return
            else:
                if self.blockchain.GetLatestBlock().index == self.MaxIndex:
                    self.status = 1
                    return
                else:
                    message = {
                        'command' : 'AskForSynchronization',
                        'ip' : {
                            'host' : self.host,
                            'port' : self.port
                        },
                        'index' : block.index+1
                    }
                    self.send_message(host, port, message)

    def initalize(self, ip):
        host = ip['host']
        port = ip['port']
        # first give the ip to all other peers
        message = {
            'command' : 'addIP',
            'ip' : {
                'host' : host,
                'port' : port
            }
        }
        for peer in self.peers:
            PeerHost = peer['host']
            PeerPort = peer['port']
            self.send_message(PeerHost, PeerPort, message)

        # then give the IP all the peers
        message = {
            'command' : 'SynchronizationIP',
            'peers' : self.peers
        }
        self.send_message(host, port, message)
        self.peers.append({'host' : host, 'port' : port})


    def broadcast_block(self, block):
        message = {
            'command': 'add_block',
            'block': block.to_dict(),
            'ip' : {
                'host' : self.host,
                'port' : self.port
            }
        }
        for peer in self.peers:
            self.send_message(peer['host'], peer['port'],message)

    def GenerateBlock(self):
        if len(self.transaction) != 0:
            block = Block(self.blockchain.GetLatestBlock().index +1, time.time(), self.transaction.pop(0), self.blockchain.GetLatestBlock().hash, -1, 0)
            self.blockchain.AddBlock(block)
            self.blockchain.PrintLatestBlock()
            self.broadcast_block(block)

    def send_transaction(self, transaction):
        message = {
            'command': 'add_transaction',
            'transaction': transaction
        }
        for peer in self.peers:
            self.send_message(peer['host'], peer['port'], message)

    def CreateTx(self):
        a = 0
        while a < 20:
            data = "Tx" + str(a)
            a+=1
            self.transaction.append(data)

    def start(self):
        listener_thread = threading.Thread(target=self.start_listening)
        listener_thread.start()
        time.sleep(3)
        if(self.port==12500):
            CreateThread = threading.Thread(target=self.CreateTx)
            CreateThread.start()

        message = {
            'command' : 'initialize',
            'ip' :{
                'host' : self.host,
                'port' : self.port
            }
        }
        self.send_message('127.0.0.1', 12500, message)
        while 1:
            if self.peers:
                if self.status == 1 and self.MaxIndex == self.blockchain.GetLatestBlock().index:
                    self.GenerateBlock()
                    time.sleep(3)
                elif self.status==2:
                    time.sleep(1)
                else:
                    ip = random.choice(self.peers)
                    print("we choose {}".format(ip))
                    message = {
                        'command' : 'AskForMaxIndex',
                        'ip' :{
                            'host' : self.host,
                            'port' : self.port
                        }
                    }
                    self.send_message('127.0.0.1', 12500, message)
                    message = {
                        'command' : 'AskForSynchronization',
                        'index' : 0,
                        'ip' : {
                            'host' : self.host,
                            'port' : self.port
                        }
                    }
                    self.send_message(ip['host'], ip['port'], message)
                    self.status = 2

def SignalHandler(sig, frame):
    global listenflag
    listenflag = False


signal.signal(signal.SIGINT, SignalHandler)

signal.signal(signal.SIGINT, SignalHandler)

x = int(input())
peer = Peer('127.0.0.1', x)
peer.start()